#include "httplib.h"
#include <thread>
#include <Svar/Svar.h>

/**
 Usages:

 1. Client
 1.1. General GET and POST
\code


\endcode
 **/



using namespace sv;
using namespace std;
using httplib::Request;
using httplib::Response;

class Server{
public:
    Server(std::string host,int port,Svar api)
        : host(host),port(port),api(api){

        server.Get("/[^.]*",[this](const Request& req,Response& res){
            toResponse(call(req,res),res);
        });
        server.Post("/[^.]*",[this](const Request& req,Response& res){
            toResponse(call(req,res),res);
        });

        worker=std::thread([this](){listen_thread();});
    }

    ~Server(){
        server.stop();
        worker.join();
    }

    Svar call(const Request& req,Response& res)
    {
        std::string path= req.path;

        if(path=="/")
            return api;
        else
            path=path.substr(1);

        for(char& c:path)
            if(c=='/') c='.';

        Svar func=api.get(path,Svar(),true);

        if(func.isUndefined())
            return {{"code","NotFound"}};

        if(!func.isFunction()) return func;

        SvarFunction& f = func.as<SvarFunction>();

        try{
            // collect arguments
            std::vector<Svar> args;
            if(collect_arguments(req,args)){ // obvious arguments call
                return f.Call(args);
            }

            if(is_common_func(f))
                return f.call(req,&res);
            else
                return f.Call(args);
        }
        catch(SvarExeption& e){
            return {{"code","SvarException"},{"message",e.what()}};
        }
        catch(std::exception& e){
            return {{"code","CXXException"},{"message",e.what()}};
        }
        return func;
    }

    bool collect_arguments(const Request& req,std::vector<Svar>& paras){
        if(req.method == "GET")
        {
            auto it = req.params.find("args");
            while(it != req.params.end()){
                paras.push_back(it->second);
                it++;
            }
            if(paras.size()) return true;

            Svar ret=Svar::object();
            for(auto it:req.params){
                if(ret[it.first].isUndefined())
                    ret[it.first]=it.second;
                else if(ret[it.first].is<std::string>())
                    ret[it.first]=Svar::array({ret[it.first],it.second});
                else
                    ret[it.first].push_back(it.second);
            }

            for(std::pair<std::string,Svar> p:ret){
                paras.push_back(sv::arg(p.first)=p.second);
            }

            return false;
        }

        std::string type = req.get_header_value("Content-Type");

        if(type != "application/json") return false;

        Svar args=Svar::parse_json(req.body)["args"];

        if(args.isArray())
        {
            paras = args.castAs<std::vector<Svar>>();
            return true;
        }
        else if(args.isObject()){
            for(std::pair<std::string,Svar> p:args){
                paras.push_back(sv::arg(p.first)=p.second);
            }
            return true;
        }

        return false;
    }

    bool is_common_func(SvarFunction& f){
        if(!f.do_argcheck) return false;
        if(!f.arg_types.size()==3) return false;
        for(Svar t:f.arg_types)
            if(t.as<SvarClass>().name() != "svar")
                return false;
        return true;
    }

    static Svar parameter(const Request& req){
        Svar ret=Svar::object();
        for(auto it:req.params){
            if(ret[it.first].isUndefined())
                ret[it.first]=it.second;
            else if(ret[it.first].is<std::string>())
                ret[it.first]=Svar::array({ret[it.first],it.second});
            else
                ret[it.first].push_back(it.second);
        }
        return ret;
    }


    void toResponse(Svar ret,Response& res){
        if(ret.isUndefined()) return;
        if(ret.is<SvarBuffer>()){
            SvarBuffer& buf=ret.as<SvarBuffer>();
            res.body=std::string();
            res.set_content((char*)buf.ptr(),buf.size(),
                            "application/x-www-form-urlencoded");
        }
        else{
            res.set_content(ret.dump_json(),"application/json");
        }
    }

    static Svar body(const Request& req){
        std::string type=req.get_header_value("Content-Type");
        if(type=="application/json")
            return Svar::parse_json(req.body);
        else{
            return {{"Content-Type",type},
                    {"body",SvarBuffer(req.body.data(),req.body.size())}};
        }
    }

    void listen_thread(){
        std::cout<<"Listening "<<host<<":"<<port<<" ..."<<std::endl;
        server.listen(host.c_str(),port);
        std::cout<<"Closed "<<host<<":"<<port<<"."<<std::endl;
    }

    std::string     host;
    int             port;
    Svar            api;
    std::thread     worker;
    httplib::Server server;
};

class HTTPURL
{
    private:
        string _protocol;// http vs https
        string _domain;  // mail.google.com
        uint16_t _port;  // 80,443
        string _path;    // /mail/
        string _query;   // [after ?] a=b&c=b

    public:
        const string &protocol;
        const string &domain;
        const uint16_t &port;
        const string &path;
        const string &query;

        HTTPURL(const string& url): protocol(_protocol),domain(_domain),port(_port),path(_path),query(_query)
        {
            string u = _trim(url);
            size_t offset=0, slash_pos, hash_pos, colon_pos, qmark_pos;
            string urlpath,urldomain,urlport;
            uint16_t default_port;

            static const char* allowed[] = { "https://", "http://", "ftp://", NULL};
            for(int i=0; allowed[i]!=NULL && this->_protocol.length()==0; i++)
            {
                const char* c=allowed[i];
                if (u.compare(0,strlen(c), c)==0) {
                    offset = strlen(c);
                    this->_protocol=string(c,0,offset-3);
                }
            }
            default_port = this->_protocol=="https" ? 443 : 80;
            slash_pos = u.find_first_of('/', offset+1 );
            urlpath = slash_pos==string::npos ? "/" : u.substr(slash_pos);
            urldomain = string( u.begin()+offset, slash_pos != string::npos ? u.begin()+slash_pos : u.end() );
            urlpath = (hash_pos = urlpath.find("#"))!=string::npos ? urlpath.substr(0,hash_pos) : urlpath;
            urlport = (colon_pos = urldomain.find(":"))!=string::npos ? urldomain.substr(colon_pos+1) : "";
            urldomain = urldomain.substr(0, colon_pos!=string::npos ? colon_pos : urldomain.length());
            this->_domain = _tolower(urldomain);
            this->_query = (qmark_pos = urlpath.find("?"))!=string::npos ? urlpath.substr(qmark_pos+1) : "";
            this->_path = qmark_pos!=string::npos ? urlpath.substr(0,qmark_pos) : urlpath;
            this->_port = urlport.length()==0 ? default_port : _atoi(urlport) ;
        };

        static std::shared_ptr<Response> Get(const std::string& url){
            HTTPURL u(url);
            httplib::Client client(u.domain.c_str(),u.port);
            auto query=u.query.empty()?"":("?"+u.query);
            return client.Get((u.path+query).c_str());
        }

        static std::shared_ptr<Response> Post(const std::string& url, const std::string &body,
                                              const char *content_type="application/json"){
            HTTPURL u(url);
            httplib::Client client(u.domain.c_str(),u.port);
            auto query=u.query.empty()?"":("?"+u.query);
            return client.Post((u.path+query).c_str(),body,content_type);
        }

    private:
        static inline string _trim(const string& input)
        {
            string str = input;
            size_t endpos = str.find_last_not_of(" \t\n\r");
            if( string::npos != endpos )
            {
                str = str.substr( 0, endpos+1 );
            }
            size_t startpos = str.find_first_not_of(" \t\n\r");
            if( string::npos != startpos )
            {
                str = str.substr( startpos );
            }
            return str;
        };
        static inline string _tolower(const string& input)
        {
            string str = input;
            std::transform(str.begin(), str.end(), str.begin(), ::tolower);
            return str;
        };
        static inline int _atoi(const string& input)
        {
            int r;
            std::stringstream(input) >> r;
            return r;
        };
};

Svar http_get(std::string url){
    auto res=HTTPURL::Get(url);
    if(!res)
        return Svar();
    try{
    std::string type=res->get_header_value("Content-Type");
    if(type=="application/json")
        return Svar::parse_json(res->body);
    else
        return res;
    }
    catch(std::exception& e){
        return Svar();
    }
}

Svar http_post(std::string url,Svar req){
    std::shared_ptr<Response> res;
    if(req.is<Request>())
    {
        Request& request=req.as<Request>();
        res=HTTPURL::Post(url,request.body,request.get_header_value("Content-Type").c_str());
    }
    else
    {
        res=HTTPURL::Post(url,req.dump_json(),"application/json");
    }

    if(!res)
        return Svar();

    std::string type=res->get_header_value("Content-Type");
    if(type=="application/json")
        return Svar::parse_json(res->body);
    else
        return res;
}

Svar http_call(std::vector<Svar> args){
    std::vector<Svar> f_args(args.begin()+1,args.end());
    return http_post(args.front().as<std::string>(),Svar({{"args",f_args}}));
}

REGISTER_SVAR_MODULE(http){
    svar["get"] =http_get;
    svar["post"]=http_post;

    SvarFunction f;
    f.do_argcheck=false;
    f.is_constructor=false;
    f.is_method=false;
    f._func=http_call;
    svar["call"]=f;

    Class<Server>("Server")
            .def("__init__",[](std::string host,int port,Svar api){
        return std::make_shared<Server>(host,port,api);
    })
            .def_readonly("port",&Server::port);

    Class<Request>("Request")
            .construct<>()
            .def("get_header_value",[](Request& self,std::string key, int id){
        return self.get_header_value(key.c_str(),id);},"id"_a=1)
            .def("set_header",[](Request& self,std::string key, std::string v){
        return self.set_header(key.c_str(),v);})
    .def("parameters",[](Request& self){return Server::parameter(self);})
    .def("method",[](const Request& self){return self.method;})
    .def("path",[](const Request& self){return self.path;})
    .def("body",[](const Request& self){return SvarBuffer(self.body.data(),self.body.size());});

    Class<Response>("Response")
      .def("body",[](const Response& self){return SvarBuffer(self.body.data(),self.body.size());});
}

EXPORT_SVAR_INSTANCE
